package com.jorado.redis;

import brave.internal.HexCodec;
import com.alibaba.fastjson.JSON;
import com.jorado.common.container.RequestID;
import com.jorado.common.exception.BaseException;
import com.jorado.common.exception.ExceptionStatusCode;
import com.jorado.common.log.Logger;
import com.jorado.common.log.LoggerFactory;
import com.jorado.common.trace.ZipkinContext;
import com.jorado.common.util.TraceContextUtil;
import com.jorado.common.util.UniqueIdUtils;
import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisDataException;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

public class RedisClientProxy implements InvocationHandler {

    public static Logger LOGGER = LoggerFactory.getLogger(RedisClientProxy.class);
    // 默认redis提示警告时间
    private static final long DEFAULT_WARN_TIME = 20;
    // 实际的类
    private RedisClientImpl redisClient;
    // 统计类
    private RedisStatisticsHandler redisStatisticsHandler;
    //
    private boolean throwExceptionFlag = false;

    public RedisClientProxy(RedisClientImpl redisClient) {
        this.redisClient = redisClient;
        this.redisStatisticsHandler = new RedisStatisticsHandler();
    }

    public RedisClientProxy(RedisClientImpl redisClient, boolean throwExceptionFlag) {
        this.redisClient = redisClient;
        this.redisStatisticsHandler = new RedisStatisticsHandler();
        this.throwExceptionFlag = throwExceptionFlag;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        // 获取当前线程的taskId
        String taskId = RequestID.getRequestID();
        if (StringUtils.isEmpty(taskId)) {
            taskId = UniqueIdUtils.generate();
            RequestID.setRequestID(taskId);
        }
        final long startTime = System.currentTimeMillis();
        String service = method.getDeclaringClass().getName();
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return this.redisClient.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return this.redisClient.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return this.redisClient.equals(args[0]);
        }
        // 需要把某些操作不允许代理
        if ("getRedisClient".equals(methodName) && parameterTypes.length == 0) {
            return this.redisClient.getRedisClient();
        }
        String ip = parseServerIp(this.redisClient);
        Object result = null;
        try {
            if (this.redisClient.getRedisClient() == null) {
                this.redisClient.initRedisClient();
            }
            result = method.invoke(this.redisClient, args);
            final long cost = System.currentTimeMillis() - startTime;
            // 如果Redis的执行时间大于20ms, 打印日志
            if (cost > DEFAULT_WARN_TIME) {
                // 打印日志
                logWarnRedis(cost, ip, service, methodName, args, result);
            } else {
                // 如果允许日志操作, 只有时间比较长的操作会打印日志
                logInfoRedis(cost, ip, service, methodName, args, result);
            }
            recordRequest(ip, methodName, RequestType.NORMAL.name(), cost);
            return result;
        } catch (InvocationTargetException exp) {
            Throwable cause = exp.getCause();
            final long cost = System.currentTimeMillis() - startTime;
            logErrorRedis(cost, ip, service, methodName, args, null, cause);
            if (cause instanceof JedisDataException) {
                RuntimeException throwExp = executeJedisDataException((JedisDataException) cause, methodName, args);
                if (throwExp != null) {
                    throw throwExp;
                }
            }
            recordRequest(ip, methodName, cause.getClass().getName(), cost);
            if (throwExceptionFlag) {
                throw new BaseException(ExceptionStatusCode.EXCEPTION_STATUS_REDIS_INT, exp);
            }
        } catch (Throwable cause) {
            final long cost = System.currentTimeMillis() - startTime;
            recordRequest(ip, methodName, cause.getClass().getName(), cost);
            logErrorRedis(cost, ip, service, methodName, args, result, cause);
            if (throwExceptionFlag) {
                throw new BaseException(ExceptionStatusCode.EXCEPTION_STATUS_REDIS_INT, cause);
            }
        }
        result = retDefValue(method);
        return result;
    }

    private void recordRequest(final String ipAddress, final String methodName, final String status, final long cost) {
        try {
            redisStatisticsHandler.statistics(ipAddress, methodName, status, cost);
        } catch (Exception exp) {
            // 忽略任何异常
        }
    }

    protected enum RequestType {
        FUSE, NORMAL, CONNECT_ERR, TIMEOUT_ERR, ERROR
    }

    private void logErrorRedis(long executeTime, String ip, String service, String method, Object[] args, Object result, Throwable exp) {
        Map<String, Object> json = new HashMap<>();
        json.put("ExecuteTime", executeTime);
        json.put("RedisIp", ip);
        json.put("Service", service);
        json.put("Method", method);
        json.put("Params", args);
        json.put("Response", result);
        json.put("Status", exp.getClass().getName());
        generateSpanContext(json, ip, method);
        LOGGER.error(JSON.toJSONString(json), exp);
    }

    private void logInfoRedis(long executeTime, String ip, String service, String method, Object[] args, Object result) {
        Map<String, Object> json = new HashMap<>();
        json.put("ExecuteTime", executeTime);
        json.put("RedisIp", ip);
        json.put("Service", service);
        json.put("Method", method);
        json.put("Params", args);
        json.put("Response", result);
        json.put("Status", "info");
        generateSpanContext(json, ip, method);
        LOGGER.info(JSON.toJSONString(json));
    }

    private void logWarnRedis(long executeTime, String ip, String service, String method, Object[] args, Object result) {
        Map<String, Object> json = new HashMap<>();
        json.put("ExecuteTime", executeTime);
        json.put("RedisIp", ip);
        json.put("Service", service);
        json.put("Method", method);
        json.put("Params", args);
        json.put("Response", result);
        json.put("Status", "warn");
        generateSpanContext(json, ip, method);
        LOGGER.warn(JSON.toJSONString(json));
    }

    private Object retDefValue(Method method) {
        final Class<?> retType = method.getReturnType();
        if (retType == boolean.class) {
            return Boolean.FALSE;
        } else if (retType == byte.class) {
            return 0;
        } else if (retType == char.class) {
            return 0;
        } else if (retType == short.class) {
            return 0;
        } else if (retType == int.class) {
            return 0;
        } else if (retType == long.class) {
            return 0L;
        } else if (retType == float.class) {
            return 0;
        } else if (retType == double.class) {
            return 0;
        }
        if ("del".equals(method.getName()) && (retType == Long.class || retType == long.class)) {
            return -1;
        }
        return null;
    }

    protected RuntimeException executeJedisDataException(JedisDataException exp, String methodName, Object[] args) {
        if (exp.getMessage() != null) {
            if (exp.getMessage().startsWith("WRONGTYPE")) {
                // 忽略异常
                Map<String, Object> logArgs = new HashMap<>();
                logArgs.put("method", methodName);
                logArgs.put("args", args);
                return new BaseException(ExceptionStatusCode.EXCEPTION_STATUS_REDIS_INT, JSON.toJSONString(logArgs));
            }
        }
        return null;
    }

    public RedisClientImpl getRedisClient() {
        return redisClient;
    }


    private String parseServerIp(RedisClientImpl redisClient) {
        if (redisClient == null) {
            return "";
        }
        Map<String, JedisPool> clients = redisClient.getRedisClient().getClusterNodes();
        TreeSet<String> sortedHosts = new TreeSet<>(clients.keySet());
        if (sortedHosts != null && !sortedHosts.isEmpty()) {
            return sortedHosts.first();
        }
        return "";
    }

    private static void generateSpanContext(Map<String, Object> json, String ip, String method) {
        try {
            if (ZipkinContext.inst.isInit()) {
                brave.Span client = TraceContextUtil.initTraceCtx(ip + ":" + method);
                String requestId = RequestID.getRequestID();
                String spanId = HexCodec.toLowerHex(client.context().spanId());
                json.put("traceId", requestId);
                json.put("spanId", spanId);
                if (client.context().parentId() != null) {
                    String parentSpanId = HexCodec.toLowerHex(client.context().parentId());
                    json.put("parentId", parentSpanId);
                }
                if (client != null) {
                    client.finish();
                }
            }
        } catch (Exception exp) {
            // 忽略异常
        }
    }

}
